/* *
 *
 *  Networkgraph series
 *
 *  (c) 2010-2019 Paweł Fus
 *
 *  License: www.highcharts.com/license
 *
 *  !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
 *
 * */
"use strict";
import H from "../../parts/Globals.js";
import U from "../../parts/Utilities.js";
var clamp = U.clamp,
  defined = U.defined,
  extend = U.extend,
  pick = U.pick,
  setAnimation = U.setAnimation;
import "./integrations.js";
import "./QuadTree.js";
var addEvent = H.addEvent,
  Chart = H.Chart;
/* eslint-disable no-invalid-this, valid-jsdoc */
H.layouts = {
  "reingold-fruchterman": function () {},
};
extend(
  /**
   * Reingold-Fruchterman algorithm from
   * "Graph Drawing by Force-directed Placement" paper.
   * @private
   */
  H.layouts["reingold-fruchterman"].prototype,
  {
    init: function (options) {
      this.options = options;
      this.nodes = [];
      this.links = [];
      this.series = [];
      this.box = {
        x: 0,
        y: 0,
        width: 0,
        height: 0,
      };
      this.setInitialRendering(true);
      this.integration = H.networkgraphIntegrations[options.integration];
      this.attractiveForce = pick(
        options.attractiveForce,
        this.integration.attractiveForceFunction
      );
      this.repulsiveForce = pick(
        options.repulsiveForce,
        this.integration.repulsiveForceFunction
      );
      this.approximation = options.approximation;
    },
    start: function () {
      var layout = this,
        series = this.series,
        options = this.options;
      layout.currentStep = 0;
      layout.forces = (series[0] && series[0].forces) || [];
      if (layout.initialRendering) {
        layout.initPositions();
        // Render elements in initial positions:
        series.forEach(function (s) {
          s.render();
        });
      }
      layout.setK();
      layout.resetSimulation(options);
      if (options.enableSimulation) {
        layout.step();
      }
    },
    step: function () {
      var layout = this,
        series = this.series,
        options = this.options;
      // Algorithm:
      layout.currentStep++;
      if (layout.approximation === "barnes-hut") {
        layout.createQuadTree();
        layout.quadTree.calculateMassAndCenter();
      }
      layout.forces.forEach(function (forceName) {
        layout[forceName + "Forces"](layout.temperature);
      });
      // Limit to the plotting area and cool down:
      layout.applyLimits(layout.temperature);
      // Cool down the system:
      layout.temperature = layout.coolDown(
        layout.startTemperature,
        layout.diffTemperature,
        layout.currentStep
      );
      layout.prevSystemTemperature = layout.systemTemperature;
      layout.systemTemperature = layout.getSystemTemperature();
      if (options.enableSimulation) {
        series.forEach(function (s) {
          // Chart could be destroyed during the simulation
          if (s.chart) {
            s.render();
          }
        });
        if (
          layout.maxIterations-- &&
          isFinite(layout.temperature) &&
          !layout.isStable()
        ) {
          if (layout.simulation) {
            H.win.cancelAnimationFrame(layout.simulation);
          }
          layout.simulation = H.win.requestAnimationFrame(function () {
            layout.step();
          });
        } else {
          layout.simulation = false;
        }
      }
    },
    stop: function () {
      if (this.simulation) {
        H.win.cancelAnimationFrame(this.simulation);
      }
    },
    setArea: function (x, y, w, h) {
      this.box = {
        left: x,
        top: y,
        width: w,
        height: h,
      };
    },
    setK: function () {
      // Optimal distance between nodes,
      // available space around the node:
      this.k = this.options.linkLength || this.integration.getK(this);
    },
    addElementsToCollection: function (elements, collection) {
      elements.forEach(function (elem) {
        if (collection.indexOf(elem) === -1) {
          collection.push(elem);
        }
      });
    },
    removeElementFromCollection: function (element, collection) {
      var index = collection.indexOf(element);
      if (index !== -1) {
        collection.splice(index, 1);
      }
    },
    clear: function () {
      this.nodes.length = 0;
      this.links.length = 0;
      this.series.length = 0;
      this.resetSimulation();
    },
    resetSimulation: function () {
      this.forcedStop = false;
      this.systemTemperature = 0;
      this.setMaxIterations();
      this.setTemperature();
      this.setDiffTemperature();
    },
    setMaxIterations: function (maxIterations) {
      this.maxIterations = pick(maxIterations, this.options.maxIterations);
    },
    setTemperature: function () {
      this.temperature = this.startTemperature = Math.sqrt(this.nodes.length);
    },
    setDiffTemperature: function () {
      this.diffTemperature =
        this.startTemperature / (this.options.maxIterations + 1);
    },
    setInitialRendering: function (enable) {
      this.initialRendering = enable;
    },
    createQuadTree: function () {
      this.quadTree = new H.QuadTree(
        this.box.left,
        this.box.top,
        this.box.width,
        this.box.height
      );
      this.quadTree.insertNodes(this.nodes);
    },
    initPositions: function () {
      var initialPositions = this.options.initialPositions;
      if (H.isFunction(initialPositions)) {
        initialPositions.call(this);
        this.nodes.forEach(function (node) {
          if (!defined(node.prevX)) {
            node.prevX = node.plotX;
          }
          if (!defined(node.prevY)) {
            node.prevY = node.plotY;
          }
          node.dispX = 0;
          node.dispY = 0;
        });
      } else if (initialPositions === "circle") {
        this.setCircularPositions();
      } else {
        this.setRandomPositions();
      }
    },
    setCircularPositions: function () {
      var box = this.box,
        nodes = this.nodes,
        nodesLength = nodes.length + 1,
        angle = (2 * Math.PI) / nodesLength,
        rootNodes = nodes.filter(function (node) {
          return node.linksTo.length === 0;
        }),
        sortedNodes = [],
        visitedNodes = {},
        radius = this.options.initialPositionRadius;
      /**
       * @private
       */
      function addToNodes(node) {
        node.linksFrom.forEach(function (link) {
          if (!visitedNodes[link.toNode.id]) {
            visitedNodes[link.toNode.id] = true;
            sortedNodes.push(link.toNode);
            addToNodes(link.toNode);
          }
        });
      }
      // Start with identified root nodes an sort the nodes by their
      // hierarchy. In trees, this ensures that branches don't cross
      // eachother.
      rootNodes.forEach(function (rootNode) {
        sortedNodes.push(rootNode);
        addToNodes(rootNode);
      });
      // Cyclic tree, no root node found
      if (!sortedNodes.length) {
        sortedNodes = nodes;
        // Dangling, cyclic trees
      } else {
        nodes.forEach(function (node) {
          if (sortedNodes.indexOf(node) === -1) {
            sortedNodes.push(node);
          }
        });
      }
      // Initial positions are laid out along a small circle, appearing
      // as a cluster in the middle
      sortedNodes.forEach(function (node, index) {
        node.plotX = node.prevX = pick(
          node.plotX,
          box.width / 2 + radius * Math.cos(index * angle)
        );
        node.plotY = node.prevY = pick(
          node.plotY,
          box.height / 2 + radius * Math.sin(index * angle)
        );
        node.dispX = 0;
        node.dispY = 0;
      });
    },
    setRandomPositions: function () {
      var box = this.box,
        nodes = this.nodes,
        nodesLength = nodes.length + 1;
      /**
       * Return a repeatable, quasi-random number based on an integer
       * input. For the initial positions
       * @private
       */
      function unrandom(n) {
        var rand = (n * n) / Math.PI;
        rand = rand - Math.floor(rand);
        return rand;
      }
      // Initial positions:
      nodes.forEach(function (node, index) {
        node.plotX = node.prevX = pick(node.plotX, box.width * unrandom(index));
        node.plotY = node.prevY = pick(
          node.plotY,
          box.height * unrandom(nodesLength + index)
        );
        node.dispX = 0;
        node.dispY = 0;
      });
    },
    force: function (name) {
      this.integration[name].apply(
        this,
        Array.prototype.slice.call(arguments, 1)
      );
    },
    barycenterForces: function () {
      this.getBarycenter();
      this.force("barycenter");
    },
    getBarycenter: function () {
      var systemMass = 0,
        cx = 0,
        cy = 0;
      this.nodes.forEach(function (node) {
        cx += node.plotX * node.mass;
        cy += node.plotY * node.mass;
        systemMass += node.mass;
      });
      this.barycenter = {
        x: cx,
        y: cy,
        xFactor: cx / systemMass,
        yFactor: cy / systemMass,
      };
      return this.barycenter;
    },
    barnesHutApproximation: function (node, quadNode) {
      var layout = this,
        distanceXY = layout.getDistXY(node, quadNode),
        distanceR = layout.vectorLength(distanceXY),
        goDeeper,
        force;
      if (node !== quadNode && distanceR !== 0) {
        if (quadNode.isInternal) {
          // Internal node:
          if (
            quadNode.boxSize / distanceR < layout.options.theta &&
            distanceR !== 0
          ) {
            // Treat as an external node:
            force = layout.repulsiveForce(distanceR, layout.k);
            layout.force(
              "repulsive",
              node,
              force * quadNode.mass,
              distanceXY,
              distanceR
            );
            goDeeper = false;
          } else {
            // Go deeper:
            goDeeper = true;
          }
        } else {
          // External node, direct force:
          force = layout.repulsiveForce(distanceR, layout.k);
          layout.force(
            "repulsive",
            node,
            force * quadNode.mass,
            distanceXY,
            distanceR
          );
        }
      }
      return goDeeper;
    },
    repulsiveForces: function () {
      var layout = this;
      if (layout.approximation === "barnes-hut") {
        layout.nodes.forEach(function (node) {
          layout.quadTree.visitNodeRecursive(null, function (quadNode) {
            return layout.barnesHutApproximation(node, quadNode);
          });
        });
      } else {
        layout.nodes.forEach(function (node) {
          layout.nodes.forEach(function (repNode) {
            var force, distanceR, distanceXY;
            if (
              // Node can not repulse itself:
              node !== repNode &&
              // Only close nodes affect each other:
              // layout.getDistR(node, repNode) < 2 * k &&
              // Not dragged:
              !node.fixedPosition
            ) {
              distanceXY = layout.getDistXY(node, repNode);
              distanceR = layout.vectorLength(distanceXY);
              if (distanceR !== 0) {
                force = layout.repulsiveForce(distanceR, layout.k);
                layout.force(
                  "repulsive",
                  node,
                  force * repNode.mass,
                  distanceXY,
                  distanceR
                );
              }
            }
          });
        });
      }
    },
    attractiveForces: function () {
      var layout = this,
        distanceXY,
        distanceR,
        force;
      layout.links.forEach(function (link) {
        if (link.fromNode && link.toNode) {
          distanceXY = layout.getDistXY(link.fromNode, link.toNode);
          distanceR = layout.vectorLength(distanceXY);
          if (distanceR !== 0) {
            force = layout.attractiveForce(distanceR, layout.k);
            layout.force("attractive", link, force, distanceXY, distanceR);
          }
        }
      });
    },
    applyLimits: function () {
      var layout = this,
        nodes = layout.nodes;
      nodes.forEach(function (node) {
        if (node.fixedPosition) {
          return;
        }
        layout.integration.integrate(layout, node);
        layout.applyLimitBox(node, layout.box);
        // Reset displacement:
        node.dispX = 0;
        node.dispY = 0;
      });
    },
    /**
     * External box that nodes should fall. When hitting an edge, node
     * should stop or bounce.
     * @private
     */
    applyLimitBox: function (node, box) {
      var radius = node.radius;
      /*
        TO DO: Consider elastic collision instead of stopping.
        o' means end position when hitting plotting area edge:

        - "inelastic":
        o
         \
        ______
        |  o'
        |   \
        |    \

        - "elastic"/"bounced":
        o
         \
        ______
        |  ^
        | / \
        |o'  \

        Euler sample:
        if (plotX < 0) {
            plotX = 0;
            dispX *= -1;
        }

        if (plotX > box.width) {
            plotX = box.width;
            dispX *= -1;
        }

        */
      // Limit X-coordinates:
      node.plotX = clamp(node.plotX, box.left + radius, box.width - radius);
      // Limit Y-coordinates:
      node.plotY = clamp(node.plotY, box.top + radius, box.height - radius);
    },
    /**
     * From "A comparison of simulated annealing cooling strategies" by
     * Nourani and Andresen work.
     * @private
     */
    coolDown: function (temperature, temperatureStep, currentStep) {
      // Logarithmic:
      /*
        return Math.sqrt(this.nodes.length) -
            Math.log(
                currentStep * layout.diffTemperature
            );
        */
      // Exponential:
      /*
        var alpha = 0.1;
        layout.temperature = Math.sqrt(layout.nodes.length) *
            Math.pow(alpha, layout.diffTemperature);
        */
      // Linear:
      return temperature - temperatureStep * currentStep;
    },
    isStable: function () {
      return (
        Math.abs(this.systemTemperature - this.prevSystemTemperature) <
          0.00001 || this.temperature <= 0
      );
    },
    getSystemTemperature: function () {
      return this.nodes.reduce(function (value, node) {
        return value + node.temperature;
      }, 0);
    },
    vectorLength: function (vector) {
      return Math.sqrt(vector.x * vector.x + vector.y * vector.y);
    },
    getDistR: function (nodeA, nodeB) {
      var distance = this.getDistXY(nodeA, nodeB);
      return this.vectorLength(distance);
    },
    getDistXY: function (nodeA, nodeB) {
      var xDist = nodeA.plotX - nodeB.plotX,
        yDist = nodeA.plotY - nodeB.plotY;
      return {
        x: xDist,
        y: yDist,
        absX: Math.abs(xDist),
        absY: Math.abs(yDist),
      };
    },
  }
);
/* ************************************************************************** *
 * Multiple series support:
 * ************************************************************************** */
// Clear previous layouts
addEvent(Chart, "predraw", function () {
  if (this.graphLayoutsLookup) {
    this.graphLayoutsLookup.forEach(function (layout) {
      layout.stop();
    });
  }
});
addEvent(Chart, "render", function () {
  var systemsStable,
    afterRender = false;
  /**
   * @private
   */
  function layoutStep(layout) {
    if (
      layout.maxIterations-- &&
      isFinite(layout.temperature) &&
      !layout.isStable() &&
      !layout.options.enableSimulation
    ) {
      // Hook similar to build-in addEvent, but instead of
      // creating whole events logic, use just a function.
      // It's faster which is important for rAF code.
      // Used e.g. in packed-bubble series for bubble radius
      // calculations
      if (layout.beforeStep) {
        layout.beforeStep();
      }
      layout.step();
      systemsStable = false;
      afterRender = true;
    }
  }
  if (this.graphLayoutsLookup) {
    setAnimation(false, this);
    // Start simulation
    this.graphLayoutsLookup.forEach(function (layout) {
      layout.start();
    });
    // Just one sync step, to run different layouts similar to
    // async mode.
    while (!systemsStable) {
      systemsStable = true;
      this.graphLayoutsLookup.forEach(layoutStep);
    }
    if (afterRender) {
      this.series.forEach(function (s) {
        if (s && s.layout) {
          s.render();
        }
      });
    }
  }
});
